Repository navigation
machine/esp32: add PWM support using the LEDC peripheral for classic esp32 board - #5669
Conversation
IMG_4246.mov |
8850e1f to
763c83b
Compare
|
Hi @deadprogram PTAL |
763c83b to
b975321
Compare
|
Thanks @zombieleet. Notes below, edited from an automated review. I built the three targets. The
The following items are not new problems in this PR. What do you think, would it be a good idea to address them now, or to leave for a separate PR?
|
|
@zombieleet did you see my most recent feedback? Thanks. |
|
Hi @deadprogram Yah, I saw it. I am currently in a different city (without my personal laptop) for company offsite. By Friday/Saturday I will make the update when I am back to Berlin. Sorry about this :( |
No worries, and thank you! 😄 |
The classic ESP32 had no PWM. This adds it.
It uses the high speed half of the LEDC peripheral. This gives 4 timers
(PWM0 to PWM3) and 8 channels. You can use any pin, because the signal
goes through the GPIO matrix.
The ESP32-C3 and the ESP32-S3 have LEDC PWM. Most of that code is the
same for all of these chips. This change uses that code again. It does
not add a second driver for the same peripheral.
Three functions in the shared file work only on the newer chips. They
move to a file for each chip.
enableClock The C3 and S3 turn on the clock in SYSTEM. This chip
uses DPORT. It also has no LEDC.CONF_CLK_EN bit.
setTimerConf The timer registers here are HSTIMER0_CONF. On the
other chips they are TIMER0_CONF.
chanOp The channel registers here are HSCH0_CONF0. On the
other chips they are CH0_CONF0.
The files are now:
machine_esp32xx_pwm.go The shared part. It builds for esp32 also.
It calls enableClock and setTimerConf.
machine_esp32xx_ls_pwm.go New. It holds the two low speed functions.
The code moved without a change. It builds
for esp32c3 and esp32s3 only.
machine_esp32_pwm.go New. The versions for the classic ESP32.
The channel and timer registers repeat at a fixed distance, so chanOp
and setTimerConf use pointer arithmetic to find them. Pin.outFunc in
machine_esp32.go does the same. Channel registers repeat every 0x14
bytes and timer registers every 0x8.
Two hardware details are important.
TICK_SEL has the opposite meaning on this chip. Here 1 selects APB_CLK
at 80MHz and 0 selects REF_TICK at 1MHz. The low speed timers on the C3
and the S3 write 0. A 0 here makes all frequencies 80 times too slow.
There is no PARA_UP bit. On the C3 and the S3 you set PARA_UP to apply a
change to a channel. High speed channels apply the change themselves at
the end of the period.
The reset of the LEDC block happens one time only. The reset clears all
timers and all channels, so a Configure of a second timer would erase
the first one.
Tests:
The examples/pwm binaries for esp32c3-supermini and xiao-esp32s3 are the
same byte for byte before and after this change. The move of the code
did not change it.
Tested on an ESP32-WROVER board. A program wrote a different value to
all 4 timers and all 8 channels through the pointer arithmetic, then
read the values back through the named registers. All of them agree.
A servo on GPIO15 turns from 0 to 180 degrees and back. The movement is
smooth. This shows that the timing is correct. A servo moves only with
pulses between 0.5ms and 2.5ms that repeat near 50Hz.
Motor control PWM (the MCPWM peripheral) is not in this change. It needs
a different API.
b975321 to
a335f5a
Compare
|
@deadprogram PTAL 🙏🏿 |
aa2ebad to
dbfa6f5
Compare
|
Thanks @zombieleet. Notes below, edited from an automated review. Almost there just a could of comments that should be adjusted:
Thank you! |
Several problems in the shared LEDC code. They are older than the PWM support for the classic ESP32, but that change makes them reachable. The four timers share one set of 8 channels in the hardware, yet each LEDCPWM had a table of its own that starts at index 0. PWM0.Channel(a) and PWM1.Channel(b) both returned channel 0 and both wrote hardware channel 0, so only one timer worked at a time. The table now belongs to the peripheral and records the timer that owns each entry. Configure cleared that whole table, which with a shared table erases the channels of the other timers, so it now releases only its own. Releasing a channel also disables its output, because the table alone does not stop the hardware from driving the pin. Set and SetInverting took a channel number without checking who owns it. One timer could reprogram another timer's channel, with the duty scaled for the wrong resolution. Both now go through LEDCPWM.owns. SetInverting did not invert. It wrote IDLE_LV, which only sets the pin level when SIG_OUT_EN is 0, so it has no effect on a running signal. The GPIO matrix does the inversion with INV_SEL in FUNCn_OUT_SEL_CFG, and the SVD already gives that bit for each chip. chanOp has no invert operation now, so the operation and its parameter are gone from all three chips. enableClock pulses the LEDC reset, which clears every timer and channel. It now runs once for the C3 and S3 as well as for the classic ESP32. Before this, a Configure of one timer erased all the others, and a call that failed the period check destroyed running outputs on its way out. A period above one second makes the frequency truncate to zero, and the divider then divides by zero. Configure returns ErrPWMPeriodTooLong. Tested on an ESP32-WROVER board. PWM0 to PWM3 gave channels 0, 1, 2 and 3 for 4 pins, where before all four gave channel 0. A pin driven at full duty read 100 percent high, and 0 percent after a second Configure released its channel. A Set from a timer that does not own the channel left the output at 100 percent, so it was refused. SetInverting took the same pin from 100 percent to 0 percent, and back to 99 percent when switched off. Configure with a period of 2 seconds returned "pwm: period too long" instead of a panic. The reads use Pin.Get on the output pin, which works because Pin.configure always sets FUN_IE.
dbfa6f5 to
ee67d19
Compare
|
@deadprogram done, thanks 🙏🏿 |
deadprogram
left a comment
There was a problem hiding this comment.
Thanks for making the requested changes. Tested on my esp32-coreboard-v2 and works as expected.
Why
The classic ESP32 is the only ESP32 chip in TinyGo without PWM. This adds it.
It uses the high speed half of the LEDC peripheral. This gives 4 timers
(
PWM0toPWM3) and 8 channels. You can use any pin, because the signal goesthrough the GPIO matrix.
This closes the ESP32 part of #5186. Thanks to @jespino for the first version
and for the hardware research. His register values were a good check while I
wrote this.
What changed
The C3 and the S3 have LEDC PWM. Most of
machine_esp32xx_pwm.gois the samefor all of these chips. This change uses that code again. It does not add a
second driver for the same peripheral.
Three functions in that file work only on the newer chips. They move to a file
for each chip.
enableClockSYSTEM. This chip usesDPORT. It also has noLEDC.CONF_CLK_ENbit.setTimerConfHSTIMER0_CONF, notTIMER0_CONF.chanOpHSCH0_CONF0, notCH0_CONF0.The files are now:
machine_esp32xx_ls_pwm.goholds code that moved out of the shared file. In thefirst commit that code is unchanged, so the C3 and the S3 keep their behaviour
and their binaries. The second commit then edits it, along with the two per chip
files, for the fixes listed below.
This also adds
src/examples/pwm/esp32-coreboard-v2.goand one line in thesmoketest-esptarget.Why these decisions
Why this is not a change to #5186.
That PR is older than the
esp32xxshared layer from #5215. It adds a type ofits own in one file of 601 lines. The tree would then have two LEDC drivers.
One uses the generated SVD setters. The other uses register offsets and masks
written by hand. The shared type keeps one driver. It also checks the register
names against the description from Espressif.
Why the high speed block.
This chip has a high speed block and a low speed block. High speed channels
apply a duty change themselves at the end of the period. There is no glitch and
no
PARA_UPbit to set. The low speed block can come later if somebody needsthe 8 more channels.
Why a new file and not a change to the shared file.
TICK_SELhas the opposite meaning on this chip.A shared
setTimerConfwould select the 1MHz clock on the ESP32. Allfrequencies would then be 80 times too slow. A file for each chip prevents
this.
Why the C3 and S3 still use a long switch.
The ESP32
chanOpfinds its register with pointer arithmetic, because thechannel registers repeat every 0x14 bytes and
outFuncinmachine_esp32.goalready works that way. The C3 and S3 registers repeat too, so the same change
would suit them, but it would rewrite files this PR only needs to touch for the
fixes. That is a good follow up.
Why GPIO18 and GPIO19 in the example.
These pins have no function at boot. They also do not go to an LED on the
board.
MCPWM is not in this change. Motor control PWM is a different peripheral. It
needs dead time and fault inputs, so it needs a different API.
Fixes in the shared code
These are older than this PR, but the ESP32 support makes them reachable. Most
came out of review.
The four timers share the eight channels. Each
LEDCPWMhad its own channeltable starting at index 0, so
PWM0.Channel(a)andPWM1.Channel(b)bothreturned channel 0 and both wrote hardware channel 0. The table now belongs to
the peripheral and records which timer owns each entry.
Configurecleared the whole table, which with a shared table erases thechannels of the other timers. It now releases only its own, and a released
channel also stops driving its pin, because the table alone does not stop the
hardware.
SetandSetInvertingtook a channel number without checking the owner.One timer could reprogram another timer's channel with the duty scaled for the
wrong resolution. Both now go through
LEDCPWM.owns.SetInvertingdid not invert. It wroteIDLE_LV, which only sets the pinlevel when
SIG_OUT_ENis 0. The GPIO matrix does the inversion withINV_SELin
FUNCn_OUT_SEL_CFG, and the SVD already gives that bit for each chip.chanOpno longer has an invert operation.enableClockpulsed the LEDC reset on everyConfigure, which clears everytimer and channel. It now runs once on all three chips. It also runs before the
period is checked, so a call returning
ErrPWMPeriodTooLongused to destroyevery running output on its way out.
A period above one second divided by zero.
freqtruncates to 0 and thedivider panics.
ConfigurereturnsErrPWMPeriodTooLong.Tests
gofmtesp32-coreboard-v2,esp32-generic,esp32c3-generic,esp32c3-supermini,esp32s3-genericandxiao-esp32s3examples/pwmbinaries foresp32c3-superminiandxiao-esp32s3are byte for byte identical todev. The second commit changes them on purposeOn that board the four timers now return channels 0, 1, 2 and 3 for four pins,
where before they all returned channel 0. Configuring a second timer leaves the
channel of the first one untouched. With the duty at the top a pin reads 100
percent high,
SetInvertingmakes it 0 percent, and switching it off againreturns it to 99 percent.
A pin driven at full duty reads 0 percent after a second
Configurereleasesits channel. A
Setfrom a timer that does not own the channel leaves theoutput at 100 percent. A
Configurewith a period of 2 seconds returnspwm: period too longinstead of panicking. Writing a different value to all 4timers and all 8 channels through the new pointer arithmetic, then reading each
back through the named registers, gives the same values.
A servo on GPIO15 turns from 0 to 180 degrees and back. The movement is smooth.
A servo moves only with pulses between 0.5ms and 2.5ms that repeat near 50Hz. A
wrong divider puts the arm in the wrong place. A wrong duty stops the movement.
One known limit
The shared
Configurecuts the clock divider to a whole number.At 50Hz with 14 bit resolution this is
97.66, and the code uses97. Theperiod is then 19.87ms and not 20ms. Pulses are about 0.67% too short.
This is the behaviour that the C3 and the S3 have now. This change does not add
it. A divider that rounds to the nearest number would give about 0.35%. That
changes the output of all three chips, so it belongs in its own PR.